﻿
using Boilen.Primitives.Members;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Threading;


namespace Boilen.Primitives.Implementers {

    /// <summary>
    /// Implements an event.
    /// </summary>
    /// <typeparam name="T">The type of the event handler.</typeparam>
    public sealed class Event<T> : AccessorImplementer<T> {

        public const string EventFieldNamePrefix = "_privateBackingFieldForEvent_";

        private const string ArgsName = "e";
        private const string EventSummaryPrefix = "Occurs when ";
        private const string HelperSummaryTemplate = "Raises the %see:parent_type.{0}% event.";

        internal const string DescriptionFormat = EventSummaryPrefix + "{0}.";

        /// <summary>The name of the observe accessor for an event.</summary>
        private const string ObserveAccessorName = "add";

        /// <summary>The name of the update accessor for an event.</summary>
        private const string UpdateAccessorName = "remove";

        private readonly Type argsType_;
        private readonly string argsTypeName_;
        private readonly string onHelperName_;
        private readonly AttributeMember onHelperAttribute_;
        private readonly EventInfo existingEvent_;
        private string helperModifiers_;
        private ConstructorInfo argsConstructor_;


        /// <summary>
        /// Gets a value indicating whether a convenience method should be added for creating the event args instance.
        /// </summary>
        public bool EnableArgsConvenienceMethod { get; set; }

        /// <summary>
        /// Gets the <see cref="EventArgs"/> type for the event.
        /// </summary>
        public Type ArgsType { get { return this.argsType_; } }

        /// <summary>
        /// Gets the name of the <see cref="EventArgs"/> type for the event.
        /// </summary>
        public string ArgsTypeName { get { return this.argsTypeName_; } }


        /// <inheritdoc/>
        protected override string FieldNamePrefix { get { return EventFieldNamePrefix; } }

        /// <inheritdoc/>
        protected override bool EnsureDescription {
            get { return base.EnsureDescription && this.existingEvent_ == null; }
        }

        /// <inheritdoc/>
        protected override IEnumerable<InitializationMember> Initializers {
            get { return Enumerable.Empty<InitializationMember>( ); }
        }

        /// <inheritdoc/>
        protected override AccessorMember Accessor {
            get {
                var argsParameter = new ParameterMember( ArgsName, this.ArgsTypeName ) {
                    Doc = new Doc( ArgsName, ArgsName, this.ArgsTypeName, this.TypeName )
                        .AddParam( ArgsName, "An instance of %see:type% that contains the event data." )
                };

                var onEventHelper = CreateOnEventHelper( argsParameter );
                var onConstructorHelper = this.CreateOnConstructorHelper( ) ?? Member.Empty;

                return new AccessorMember( this.AccessorName, this.TypeName ) {
                    Doc = this.CreateDoc( )
                        .AddSummary( DescriptionFormat, this.Description ),
                    Modifiers = this.AccessorModifiers + " event",
                    ObserveMember = this.CreateAccessorBlock(
                        ObserveAccessorName,
                        string.Format( "this.{0} += value;", this.FieldName )
                    ),
                    UpdateMember = this.CreateAccessorBlock(
                        UpdateAccessorName,
                        string.Format( "this.{0} -= value;", this.FieldName )
                    ),
                    Helpers = { onEventHelper, onConstructorHelper }
                };
            }
        }


        /// <inheritdoc/>
        public Event( PartialType parent, string name, string description )
            : base( parent, name, description ) {
            this.EnableArgsConvenienceMethod = true;

            var invokeMethod = typeof( T ).GetMethod( "Invoke" );
            var eventArgsParameter = invokeMethod.GetParameters( ).Skip( 1 ).Single( );

            this.argsType_ = eventArgsParameter.ParameterType;
            this.argsTypeName_ = this.Parent.TypeRepository.GetTypeName( this.argsType_ );
            this.onHelperName_ = "On" + this.AccessorName;
            this.onHelperAttribute_ = AttributeMember.SuppressMessage(
                "Microsoft.Design", "CA1030:UseEventsWhereAppropriate", "Supports the " + this.AccessorName + " event."
            );
        }

        /// <inheritdoc/>
        /// <param name="existingEvent">An existing event to use as a prototype.</param>
        public Event( PartialType parent, EventInfo existingEvent )
            : this( parent, GetExistingEventName( existingEvent ), GetExistingEventDescription( existingEvent ) ) {
            this.existingEvent_ = existingEvent;
        }


        /// <inheritdoc/>
        public override void Prepare( ) {
            base.Prepare( );

            this.Parent.TypeRepository.AddNamespace( typeof( Interlocked ) );

            Accessibility helperAccessibility =
                this.Parent.IsSealed ? Accessibility.Private :
                this.Accessibility == Accessibility.Public ? Accessibility.Protected :
                this.Accessibility;
            this.helperModifiers_ = GetAccessibilityValue( helperAccessibility );

            if( this.EnableArgsConvenienceMethod ) {
                this.argsConstructor_ = this.ArgsType.GetConstructors( )
                    .Where( c => c.GetParameters( ).Length > 0 )
                    .OrderByDescending( c => c.GetParameters( ).Length )
                    .FirstOrDefault( );

                // Ensure all parameter types are registered.
                if( this.argsConstructor_ != null )
                    foreach( var parameter in this.argsConstructor_.GetParameters( ) )
                        this.Parent.TypeRepository.AddNamespace( parameter.ParameterType );
            }
        }

        /// <summary>
        /// Assigns the value used for the <see cref="EnableArgsConvenienceMethod"/> property.
        /// </summary>
        [DefaultValue( "True" )]
        public Event<T> SetEnableArgsConvenienceMethod( bool enable ) {
            this.EnableArgsConvenienceMethod = enable;
            return this;
        }


        // "virtual void On[EventName]([EventArgs] e)" helper
        private MethodMember CreateOnEventHelper( ParameterMember argsParameter ) {
            var onBody = new BlockMember( this.onHelperName_, w => {
                const string HandlerName = "handler";
                w.WriteLine( "{0} {1} = Interlocked.CompareExchange(ref this.{2}, null, null);", this.TypeName, HandlerName, this.FieldName );
                w.WriteLine( "if (!object.ReferenceEquals({0}, null))", HandlerName );
                using( Enclose.Braces( w ) ) { w.WriteLine( "{0}(this, {1});", HandlerName, ArgsName ); }
            } ).AddGuards( new[] { Guard.NotNull( this.CreateDoc( ), ArgsName, forceDescription: true ) } );

            return new MethodMember( this.onHelperName_, "void", onBody ) {
                Modifiers = this.helperModifiers_ + (this.Parent.IsSealed ? "" : " virtual"),
                Doc = this.CreateDoc( this.onHelperName_ )
                    .AddSummary( HelperSummaryTemplate, this.AccessorName )
                    .UseDocMembers( ),
                Parameters = { argsParameter }
            };
        }

        // "void On[EventName]([EventArgs parameters])" helper
        private MethodMember CreateOnConstructorHelper( ) {
            // If method or parameters are undocumented, skip constructor-based helper.
            bool argsAreDocumented = XmlDocumentation.HasXmlDocumentation( this.ArgsType.Assembly );
            if( !argsAreDocumented || this.argsConstructor_ == null )
                return null;

            var parameters = MethodMember.GetParameters(
                this.argsConstructor_,
                ( string name, Type type, string description ) => string.IsNullOrEmpty( description ) ? null : this.CreateParameter( name, type, description )
            );

            if( parameters.Any( p => p == null ) )
                return null;


            // Build constructor-based helper.
            string argsArgumentString = Util.Join( parameters.Select( p => p.Name ), ", " );
            var onConstructorBody = new BlockMember( this.onHelperName_, w => {
                w.WriteLine( "{0} {1} = new {0}({2});", this.ArgsTypeName, ArgsName, argsArgumentString );
                w.WriteLine( "this.On{0}({1});", this.AccessorName, ArgsName );
            } );

            return new MethodMember( this.onHelperName_, "void", onConstructorBody ) {
                Modifiers = this.helperModifiers_,
                Doc = this.CreateDoc( this.onHelperName_ )
                    .AddSummary( HelperSummaryTemplate, this.AccessorName )
                    .UseDocMembers( ),
                Attributes = { this.onHelperAttribute_ }
            }.AddParameters( parameters );
        }


        private static string GetExistingEventName( EventInfo existingEvent ) {
            Ensure.NotNull( existingEvent );

            string eventName = existingEvent.Name;
            string eventImplementerName = char.ToLowerInvariant( eventName[0] ) + eventName.Substring( 1 );
            return eventImplementerName;
        }

        private static string GetExistingEventDescription( EventInfo existingEvent ) {
            Ensure.NotNull( existingEvent );

            string summary = XmlDocumentation.GetSummary( existingEvent );
            Ensure.ArgSatisfies( summary.StartsWith( EventSummaryPrefix ), "existingEvent", "Documentation for event {0} does not begin with expected '{1}' text.", existingEvent.Name, EventSummaryPrefix );

            string description = summary.Substring( EventSummaryPrefix.Length ).TrimEnd( '.' );
            return description;
        }

    }

}
